/*
    Copyright (C) 2013 maik.jablonski@jease.org

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jfix.util;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Common utility-methods to handle reflection.
 */
public class Reflections {

	/**
	 * Returns all fields of the referring class which might hold references to
	 * given reference object. The reference object is used as "template object"
	 * (and not as class) to make use of the
	 * java.lang.reflect.Type.isInstance()-method.
	 */
	public static Set<Field> getReferringFields(Class referringClazz,
			Object reference) {
		Set<Field> referringFields = new HashSet<Field>();
		for (Field possibleReferringField : getFields(referringClazz)) {
			if (isAssignable(reference.getClass(), possibleReferringField)) {
				referringFields.add(possibleReferringField);
			}
		}
		return referringFields;
	}

	/**
	 * Returns true if given possible referrer is referencing given reference
	 * object in one or more of the given referring fields.
	 */
	public static boolean isReferrer(Object possibleReferrer,
			Set<Field> referringFields, Object reference) {
		try {
			for (Field field : referringFields) {
				Object fieldValue = field.get(possibleReferrer);
				if (fieldValue != null
						&& (fieldValue == reference || (field.getType()
								.isArray() && Arrays.contains(
								(Object[]) fieldValue, reference)))) {
					return true;
				}
			}
			return false;
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * Returns all interfaces and superclasses implemented by a given class.
	 */
	public static Set<Class> getSuperClassesAndInterfaces(Class clazz) {
		Set<Class> result = new HashSet<Class>();
		if (clazz != null) {
			result.add(clazz);
			for (Class interfaceClass : clazz.getInterfaces()) {
				result.addAll(getSuperClassesAndInterfaces(interfaceClass));
			}
			result.addAll(getSuperClassesAndInterfaces(clazz.getSuperclass()));
		}
		return result;
	}

	/**
	 * Returns all fields of a given class (including fields from superclasses).
	 */
	public static Set<Field> getFields(Class clazz) {
		Set<Field> result = new HashSet<Field>();
		for (Class superClass : getSuperClassesAndInterfaces(clazz)) {
			for (Field field : superClass.getDeclaredFields()) {
				field.setAccessible(true);
				result.add(field);
			}
		}
		return result;
	}

	/**
	 * Retrieves all objects of given class-type which are referred by all the
	 * objects in given collection.
	 */
	public static Set<Object> getReferredObjects(Collection objects,
			Class objectClass) {
		try {
			Set result = new HashSet();
			for (Object object : objects) {
				for (Field field : Reflections.getFields(object.getClass())) {
					if (isAssignable(objectClass, field)) {
						Object fieldValue = field.get(object);
						if (fieldValue != null) {
							if (field.getType().isArray()) {
								for (Object arrayElement : (Object[]) fieldValue) {
									result.add(arrayElement);
								}
							} else {
								result.add(fieldValue);
							}
						}
					}
				}
			}
			return result;
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * Returns all instanceable (sub-)classes of given type in given package.
	 */
	public static <E> E[] find(Class<E> classType, Package pckage) {
		File directory;
		try {
			String name = "/" + pckage.getName().replace('.', '/');
			directory = new File(classType.getResource(name).toURI());
		} catch (URISyntaxException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
		List<E> result = new ArrayList();
		if (directory.exists()) {
			String[] files = directory.list();
			for (int i = 0; i < files.length; i++) {
				if (files[i].endsWith(".class")) {
					String classname = files[i].substring(0,
							files[i].length() - 6);
					try {
						Object o = Class.forName(
								pckage.getName() + "." + classname)
								.newInstance();
						if (classType.isInstance(o)) {
							result.add((E) o);
						}
					} catch (ClassNotFoundException cnfex) {
						System.err.println(cnfex);
					} catch (InstantiationException iex) {
					} catch (IllegalAccessException iaex) {
					}
				}
			}
		}
		Collections.sort(result, new Comparator() {
			public int compare(Object o1, Object o2) {
				return o1.getClass().getSimpleName()
						.compareTo(o2.getClass().getSimpleName());
			}
		});
		return jfix.util.Arrays.cast(result, classType);
	}

	/**
	 * Returns all instanceable (sub-)classes of given type contained in the
	 * package of given type.
	 */
	public static <E> E[] find(Class<E> classType) {
		return find(classType, classType.getPackage());
	}

	/**
	 * Returns a new instance for given clazz.
	 */
	public static Object newInstance(Class clazz) {
		try {
			return clazz.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * Returns a new instance for given fully qualified classname.
	 */
	public static Object newInstance(String classname) {
		try {
			return Class.forName(classname).newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * Returns true if given class can be assigned to given field (which might
	 * be a simple field or array).
	 */
	public static boolean isAssignable(Class clazz, Field field) {
		return clazz.isAssignableFrom(field.getType())
				|| (field.getType().isArray() && clazz.isAssignableFrom(field
						.getType().getComponentType()));
	}

	/**
	 * Initializes all declared static string fields in given class with name of
	 * fields.
	 */
	public static void init(Class clazz) {
		for (Field field : clazz.getDeclaredFields()) {
			field.setAccessible(true);
			try {
				field.set(null, field.getName());
			} catch (Exception e) {
			}
		}
	}
}
